/**
 * 任务模型
 *
 * Created by snowingsea on 2020/03/03.
 */
const path = require('path');
const moment = require('moment');
const mongoose = require('mongoose');
const escapeRegexp = require('escape-regexp');
const fs = require('fs-extra');
const Filter = require('./mongo/filter');
const Task = require('./mongo/task');
const License = require('./mongo/license');
const TaskLog = require('./mongo/task-log');
const Project = require('./mongo/project');
const ProjectVersion = require('./mongo/project-version');
const File = require('./mongo/file');
const Func = require('./mongo/function');
const FunctionVariable = require('./mongo/function-variable');
const TestCase = require('./mongo/test-case');
const numbers = require('./numbers');
const hydra = require('./hydra');
const publisher = require('./redis/publisher');
const common = require('./common');
const utility = require('./utility');
const fileElements = require('./file-elements');
const projectVersions = require('./project-versions');
const amqp = require('./amqp');
const { eventHub, eventKind } = require('./event-hub');
const logger = require('./logger');
const files = require('./files');
const functions = require('./functions');
const gitClient = require('./git/git-client');
const cacheUtil = require('./../cacheUtil');
const PhoenixError = require('./phoenix-error');

/**
 * 任务类型
 * @type {Object}
 */
const taskType = {
  parse: 1,
  testCaseGenerate: 2,
  testCaseRun: 3,
  autoTest: 4,
  analyze: 5,
  regressionAnalyze: 6, // 一键回归任务
  exportTargetMachineTests: 7, // 导出目标机测试脚本
  autoRegression: 8,
  testCaseMigrate: 9,
  autoTestAndFilter: 10,
  runtimeCheck: 11, // 运行时检查
  instrument: 12, // 插桩代码
  codeInstrumentationReportGen: 13, // 函数插桩报告生成
};

/**
 * 测试类型
 * @type {Object}
 */
const testType = {
  analyze: 1,
  test: 2,
  integration: 3,
};

/**
 * 任务状态
 * @type {Object}
 */
const taskStatusKind = {
  waiting: 0,
  running: 1,
  finished: 2,
  suspending: 3,
  suspended: 4,
  canceling: 5,
  canceled: 6,
};

const subTaskStatusKind = {
  running: 1,
  finished: 2,
};
/**
 * 获取进度
 * @param {Object} task 任务
 * @return {Number}
 */
function getProgress(task) {
  task = task.toObject ? task.toObject() : task;
  if (!task) {
    return 0;
  }
  if (task.status === taskStatusKind.finished) {
    return 1;
  }
  if (!utility.isArrayType(task.taskId)) {
    task.taskId = [task.taskId];
  }
  task.subTasks = task.subTasks || [];
  if (!task.taskId.length) {
    return 0;
  }
  let currentProgress = task.progress;
  if (task.lastRunFunctionId) {
    // 执行中
    const firstRunIdx = task.functionIds.findIndex((fid) => fid.equals(task.firstRunFunctionId));
    const lastRunIdx = task.functionIds.findIndex((fid) => fid.equals(task.lastRunFunctionId));
    const runCnt = lastRunIdx - firstRunIdx + 1;
    currentProgress = (firstRunIdx + task.progress * runCnt) / task.functionIds.length;
  }
  return (task.taskId.length - 1 + currentProgress) / (task.taskId.length + task.subTasks.length);
}

/**
 * 获取花费时间
 * @param {Task} task 任务信息
 * @return {Number}
 */
function getSpentTime(task) {
  let { spent } = task;
  if (task.status === taskStatusKind.running) {
    spent += Date.now() - task.resumeAt.getTime();
  }
  if (spent < 0) {
    return 0;
  }
  return spent;
}

/**
 * 获取测试类型
 * @param {Task} task 任务信息
 * @return {Promise}
 */
function getTestType(task) {
  if (task.testType) {
    return task.testType;
  }
  switch (task.type) {
    case taskType.analyze:
      return testType.analyze;
    case taskType.regressionAnalyze:
      return testType.analyze;
    default:
      return testType.test;
  }
}

function getTaskTypesByTestKind(kind) {
  kind = Number(kind);
  switch (kind) {
    case testType.analyze:
      return [
        taskType.parse,
        taskType.analyze,
        taskType.regressionAnalyze,
        taskType.runtimeCheck,
        taskType.instrument,
        taskType.codeInstrumentationReportGen,
      ];
    case testType.test:
      return [
        taskType.parse,
        taskType.testCaseGenerate,
        taskType.testCaseRun,
        taskType.exportTargetMachineTests,
        taskType.testCaseMigrate,
        taskType.autoRegression,
        taskType.autoTest,
        taskType.autoTestAndFilter,
        taskType.instrument,
        taskType.codeInstrumentationReportGen,
      ];
    case testType.integration:
      return [
        taskType.parse,
        taskType.testCaseGenerate,
        taskType.testCaseRun,
        taskType.testCaseMigrate,
        taskType.autoRegression,
        taskType.exportTargetMachineTests,
        taskType.autoTest,
        taskType.instrument,
        taskType.codeInstrumentationReportGen,
      ];
    default:
      return [];
  }
}

/**
 * 获取json信息
 * @param {Task} task 任务信息
 * @returns {Object}
 */
function getJson(task) {
  const json = {
    taskId: task._id,
    number: task.number,
    type: task.type,
    testType: getTestType(task),
    status: task.status,
    progress: getProgress(task),
    projectId: task.projectId,
    projectName: task.projectName,
    versionId: task.versionId,
    masterVersionId: task.version ? task.version.masterVersionId : undefined,
    versionName: task.versionName,
    fileCount: task.fileIds ? task.fileIds.length : 0,
    functionCount: task.functionIds ? task.functionIds.length : 0,
    testCaseCount: task.testIds ? task.testIds.length : 0,
    createAt: task._id.getTimestamp().getTime(), // eslint-disable-line no-underscore-dangle
    spent: getSpentTime(task),
  };
  if (task.finishAt) {
    json.finishAt = task.finishAt.getTime();
  }
  if (task.tester) {
    json.tester = task.tester;
  }
  return json;
}

/**
 * 获取任务
 * @param {String} taskId 任务id
 * @return {Promise}
 */
function getTask(taskId) {
  return Task.findOne({ _id: mongoose.Types.ObjectId(taskId) });
}

/**
 * 获取任务
 * @param {String} taskId 核心层任务id
 * @return {Promise}
 */
function getTaskByHydraTaskId(taskId) {
  return Task.findOne({ taskId });
}

/**
 * 子任务是否完成
 * @param {Object} task 任务
 * @return {Boolean}
 */
function isSubTaskFinished(task) {
  return task.subTaskStatus === subTaskStatusKind.finished;
}

/**
 * 获取任务列表请求约束
 * @param {Array|Number} types 任务类型
 * @param {Array|Number} status 任务状态
 * @param {Number} testType 测试类型
 * @param {String} q 搜索关键词
 * @return {Object}
 */
function getTaskOptions({
  types = null,
  status = null,
  testType = null,
  q = null,
}) {
  const options = {};
  // 任务类型
  if (types) {
    if (!utility.isArrayType(types)) {
      types = [types];
    }
    options.type = { $in: types.map((type) => Number.parseInt(type.toString(), 10)) };
  } else if (testType) { // 测试类型
    options.type = { $in: getTaskTypesByTestKind(testType) };
  }
  // 任务状态
  if (status) {
    if (!utility.isArrayType(status)) {
      status = [status];
    }
    options.status = { $in: status.map((item) => Number.parseInt(item.toString(), 10)) };
  }
  // 关键词搜索
  if (q) {
    options.$or = [
      { projectName: { $regex: `.*${escapeRegexp(q)}.*` } },
      { versionName: { $regex: `.*${escapeRegexp(q)}.*` } },
      // { taskId: { $regex: `.*${escapeRegexp(q)}.*` } },
    ];
  }
  return options;
}

/**
 * 筛选任务列表
 * @param {Object} pagination 分页信息
 * @param {Object} options 用户筛选项
 * @return {Promise}
 */
function filter(_pagination = {}, options = {}) {
  const pagination = { ..._pagination };
  const { sortBy } = pagination;
  if (!sortBy || sortBy === 'createAt') {
    pagination.sortBy = '_id';
  }
  if (sortBy === 'finishAt') {
    pagination.sortBy = 'finishSortBy';
  }
  return Filter.filter(Task, {
    ...pagination,
    useAggregate: true,
  }, options, [{
    $lookup: {
      from: 'projectversions', localField: 'versionId', foreignField: '_id', as: 'version',
    },
  }, {
    $unwind: '$version',
  }]);
}

/**
 * 任务数目
 * @param {Object} options 用户筛选项
 * @return {Promise}
 */
function getCount(options = {}) {
  return Task.countDocuments(options);
}

/**
 * 更新任务状态
 * @param {String} taskId 任务id
 * @param {Number} status 任务状态
 * @return {Promise}
 */
function updateTaskStatus(taskId, status) {
  return new Promise((resolve, reject) => {
    status = Number.parseInt(status.toString(), 10);
    let removeQueue = false;
    const conditions = {
      _id: mongoose.Types.ObjectId(taskId),
      $and: [
        { status: { $ne: taskStatusKind.finished } },
        { status: { $ne: taskStatusKind.canceled } },
      ],
    };
    switch (status) {
      case taskStatusKind.waiting:
        conditions.status = taskStatusKind.waiting;
        break;
      case taskStatusKind.finished:
        removeQueue = true;
        conditions.$and.push({ status: { $ne: taskStatusKind.canceling } });
        conditions.$and.push({ status: { $ne: taskStatusKind.suspending } });
        conditions.$and.push({ status: { $ne: taskStatusKind.suspended } });
        break;
      case taskStatusKind.running:
      case taskStatusKind.suspending:
      case taskStatusKind.suspended:
        conditions.$and.push({ status: { $ne: taskStatusKind.canceling } });
        break;
      case taskStatusKind.canceling:
        break;
      case taskStatusKind.canceled:
        removeQueue = true;
        break;
      default:
        break;
    }

    let task;
    const doc = { status, finishSortBy: status };
    getTask(taskId)
      .then((res) => {
        task = res;
        switch (status) {
          case taskStatusKind.finished:
            doc.finishAt = moment();
            doc.progress = 1;
            doc.finishSortBy = moment().format('X');
            if (task.status === taskStatusKind.running) {
              doc.$inc = { spent: doc.finishAt - task.resumeAt };
            }
            break;
          case taskStatusKind.suspended:
          case taskStatusKind.canceled:
            if (task.status === taskStatusKind.running) {
              doc.$inc = { spent: moment() - task.resumeAt };
            }
            break;
          case taskStatusKind.running:
            if (task.status !== taskStatusKind.running && task.status !== taskStatusKind.waiting) {
              doc.resumeAt = moment();
            }
            break;
          default:
            break;
        }
        return Task.findOneAndUpdate(conditions, doc, { new: true, rawResult: true });
      })
      .then(async (res) => {
        if (res.lastErrorObject.n === 1) {
          if (removeQueue) {
            // 删除任务独有队列
            const queue = amqp.getTaskQueue(taskId);
            await amqp.removeQueue(queue);
          }
          doc.creatorId = task.creatorId;
          doc.versionId = task.versionId;
          doc.taskId = task._id;
          doc.taskType = task.type;
          doc.progress = getProgress(res.value);
          doc.spent = getSpentTime(res.value);
          if (res.value.finishAt) {
            doc.finishAt = res.value.finishAt.getTime();
          }
          delete doc.$inc;
          publisher.publishTaskMessage('update', doc);
        }
        return Promise.resolve(res);
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 更新项目名称
 * @param {String} projectId 项目id
 * @param {String} projectName 项目名
 * @return {Promise}
 */
function updateProjectName(projectId, projectName) {
  projectId = mongoose.Types.ObjectId(projectId);
  Task.updateMany({ projectId }, { projectName });
}

/**
 * 更新任务进度
 * @param {String} taskId 项目id
 * @param {Number} progress 进度
 * @return {Promise}
 */
function updateProgress(taskId, progress) {
  return Task.findOneAndUpdate(
    { _id: mongoose.Types.ObjectId(taskId) },
    { progress },
    { new: true }
  )
    .then((task) => {
      const doc = {};
      doc.status = task.status;
      doc.creatorId = task.creatorId;
      doc.versionId = task.versionId;
      doc.taskId = task._id;
      doc.taskType = task.type;
      doc.progress = getProgress(task);
      doc.spent = getSpentTime(task);
      publisher.publishTaskMessage('update', doc);
    });
}

/**
 * 更新项目版本名称
 * @param {String} versionId 项目id
 * @param {String} versionName 项目版本名
 * @return {Promise}
 */
function updateProjectVersionName(versionId, versionName) {
  versionId = mongoose.Types.ObjectId(versionId);
  Task.updateMany({ versionId }, { versionName });
}

/**
 * 更新子任务状态
 * @param {String} taskId 任务id
 * @param {Number} subTaskStatus 子任务状态
 * @return {Promise}
 */
function updateSubTaskStatus(taskId, subTaskStatus) {
  return Task.findOneAndUpdate({ _id: mongoose.Types.ObjectId(taskId) }, { $set: { subTaskStatus } }, { new: true });
}

/**
 * 编辑任务
 * @param {String} taskId 任务id
 * @param {Number} status 任务状态
 * @return {Promise}
 */
function updateTask(taskId, { status }) {
  if (status) {
    status = Number.parseInt(status.toString(), 10);
    switch (status) {
      case taskStatusKind.running:
      case taskStatusKind.canceled:
      case taskStatusKind.suspended:
        break;
      default:
        return Promise.resolve();
    }
    let task;
    return new Promise((resolve, reject) => {
      updateTaskStatus(taskId, status)
        .then((res) => {
          const { lastErrorObject, value } = res;
          if (lastErrorObject.n === 0) {
            return Promise.resolve();
          }
          task = value;
          const hydraTaskId = task.taskId.pop();
          if (status === taskStatusKind.running && task.subTaskStatus === subTaskStatusKind.finished) {
            // eslint-disable-next-line no-use-before-define
            return runNextTask(task);
          }
          // check if taskId is valid or not
          if (!hydraTaskId || !hydraTaskId.toString().match(/[a-f0-9]{24}/i)) {
            return Promise.resolve();
          }
          switch (status) {
            case taskStatusKind.running:
              return hydra.resumeTask(hydraTaskId);
            case taskStatusKind.canceled:
              return hydra.cancelTask(hydraTaskId);
            case taskStatusKind.suspended:
              return hydra.suspendTask(hydraTaskId);
            default:
              return Promise.resolve();
          }
        })
        .then(resolve)
        .catch((err) => {
          if (err instanceof hydra.types.TaskNotFound) {
            if (task.subTasks.length > 0) {
              return resolve();
            }
            return updateTaskStatus(taskId, taskStatusKind.canceled).then(resolve).catch(reject);
          }
          return reject(err);
        });
    });
  }
  return Promise.resolve();
}

/**
 * 更新任务id
 * @param {Object} task 任务信息
 * @param {String} newTaskId 新任务id
 * @return {Promise}
 */
function updateTaskId(task, newTaskId, pop = false) {
  if (pop) {
    task.taskId.pop();
  }
  if (newTaskId) {
    task.taskId.push(newTaskId);
  }
  return Task.findOneAndUpdate(
    {
      _id: task._id,
      $and: [
        { status: { $ne: taskStatusKind.finished } },
        { status: { $ne: taskStatusKind.canceled } },
        { status: { $ne: taskStatusKind.canceling } },
      ],
    },
    { taskId: task.taskId },
    { new: true, rawResult: true }
  );
}

/**
 * 批量更新某个用户的任务
 * @param {String} userId 用户id
 * @param {Number} status 任务状态
 * @return {Promise}
 */
function updateTasks(userId, { status }) {
  if (status) {
    status = Number.parseInt(status.toString(), 10);
    return new Promise((resolve, reject) => {
      const conditions = {
        $and: [
          { status: { $ne: taskStatusKind.finished } },
          { status: { $ne: taskStatusKind.canceled } },
          { status: { $ne: taskStatusKind.canceling } },
        ],
      };
      if (userId) {
        conditions.creatorId = mongoose.Types.ObjectId(userId);
      }
      Task.find(conditions)
        .then((tasks) => {
          function step(index) {
            if (index >= tasks.length) {
              resolve();
              return;
            }
            updateTask(tasks[index]._id, { status })
              .then(() => {
                step(index + 1);
              })
              .catch(reject);
          }
          step(0);
        })
        .then(resolve)
        .catch(reject);
    });
  }
  return Promise.resolve();
}

/**
 * 添加任务
 * @param {Number} type 任务类型
 * @param {Number} compilingFileIds 编译文件id列表
 * @param {Array} fileIds 文件id列表
 * @param {Array} functionIds 文件id列表
 * @param {Array} testIds 测试用例id列表
 * @param {Array} subTasks 子任务
 * @param {String} currentSubTask 子任务
 * @param {Boolean} coverageStatistics 是否收集覆盖率
 * @param {Array} partiallyExecutedIds 已处理的id
 * @param {Array} ruleIds 运行时检查规则id
 * @param {String} codeInstrumentationId 插桩id
 * @param {Object} project 项目
 * @param {Object} projectVersion 项目版本
 * @param {Object} creator 创建用户
 * @param {String} taskId 任务id
 * @return {Promise}
 */
function addTask(
  type,
  {
    compilingFileIds = [],
    fileIds = [],
    functionIds = [],
    testIds = [],
    subTasks = [],
    currentSubTask = undefined, // 第一个执行的子任务
    coverageStatistics = true,
    partiallyExecutedIds = undefined,
    ruleIds,
    codeInstrumentationId,
  },
  project,
  projectVersion,
  creator,
  taskId = []
) {
  let testTypeValue;
  switch (projectVersion.versionType) {
    case projectVersions.versionType.staticAnalyze:
      testTypeValue = testType.analyze;
      break;
    case projectVersions.versionType.integrationTest:
      testTypeValue = testType.integration;
      break;
    default:
      testTypeValue = testType.test;
      break;
  }
  return new Promise((resolve, reject) => {
    const projectId = project._id; // eslint-disable-line no-underscore-dangle
    const versionId = projectVersion._id; // eslint-disable-line no-underscore-dangle
    const doc = {
      taskId,
      type,
      testType: testTypeValue,
      creatorId: creator._id,
      projectId,
      projectName: project.projectName,
      versionId,
      versionName: projectVersion.versionName,
      compilingFileIds,
      fileIds,
      functionIds,
      codeInstrumentationId,
      testIds,
      subTasks,
      currentSubTask,
      resumeAt: moment(),
      coverageStatistics: !!Number(coverageStatistics),
      ruleIds,
    };
    if (partiallyExecutedIds) {
      doc.partiallyExecutedIds = partiallyExecutedIds;
    }
    if (ruleIds) {
      doc.ruleIds = ruleIds;
    }
    let task;
    numbers.next(numbers.getTaskNumberName(project.creatorId))
      .then((res) => {
        doc.number = res.number;
        task = new Task(doc);
        return task.save();
      })
      .then(async (res) => {
        if (res.silence !== true) {
          doc.taskId = res._id;
          delete doc.fileIds;
          delete doc.functionIds;
          delete doc.testIds;
          delete doc.resumeAt;
          delete doc.number;
          delete doc.partiallyExecutedIds;
          publisher.publishTaskMessage('add', doc);
        }
        // 创建任务独有队列
        await amqp.createTaskQueue(res._id);
      })
      .then(() => resolve(task))
      .catch(reject);
  });
}

/**
 * 添加任务日志
 * @param {String} projectId 项目id
 * @param {String} versionId 项目版本id
 * @param {String} taskId 任务id
 * @param {String} type 任务类型
 * @param {String|Object} content 任务详情
 * @return {Promise}
 */
function addTaskLog(projectId, versionId, taskId, type, content) {
  // 去除带'.'的键值
  const list = [content];
  while (list.length > 0) {
    const item = list.pop();
    if (utility.isObjectType(item)) {
      Object.keys(item).forEach((key) => {
        if (key.indexOf('.') >= 0) {
          const newKey = common.renameMongoKey(key);
          item[newKey] = item[key];
          delete item[key];
          key = newKey;
        }
        list.push(item[key]);
      });
    } else if (utility.isArrayType(item)) {
      item.forEach((element) => {
        list.push(element);
      });
    }
  }
  const taskLog = new TaskLog({
    projectId: mongoose.Types.ObjectId(projectId),
    versionId: mongoose.Types.ObjectId(versionId),
    taskId,
    type,
    content,
  });
  return taskLog.save();
}

/**
 * 提交解析任务
 * @param {Task} task 任务
 * @param {Array} files 文件列表
 * @param {Object} project 项目信息
 * @param {Object} projectVersion 项目版本信息
 * @return {Promise}
 */
async function submitParseTask(task, files, project, projectVersion) {
  let hydraTask;
  if (projectVersion.versionType === projectVersions.versionType.integrationTest) {
    hydraTask = await hydra.integrationParse(files, project, projectVersion, task);
  } else {
    hydraTask = await hydra.parse(files, project, projectVersion, task);
  }
  if (!hydraTask) {
    return updateTaskStatus(task._id, taskStatusKind.finished);
  }
  const projectVersionId = projectVersion._id;
  const taskId = hydraTask.task_id;
  const versionDocument = { $set: {} };
  versionDocument.$set[`tasks.${taskId}`] = true;
  versionDocument.$set['eventReminder.parsed'] = true;
  const promises = [
    updateTaskId(task, taskId),
    File.updateMany({ _id: { $in: task.compilingFileIds } }, { parsingTaskId: taskId }),
    ProjectVersion.updateOne({ _id: projectVersionId }, versionDocument),
  ];
  return Promise.all(promises);
}

async function submitInstrumentTask(task, fileFuncs, project, projectVersion) {
  const hydraTask = await hydra.instrument(fileFuncs, project, projectVersion, task);
  if (!hydraTask) {
    return updateTaskStatus(task._id, taskStatusKind.finished);
  }
  const taskId = hydraTask.task_id;
  const versionDocument = { $set: {} };
  versionDocument.$set[`tasks.${taskId}`] = true;
  const projectVersionId = projectVersion._id;
  await updateTaskId(task, taskId);
  await Promise.all([
    Task.updateOne(
      { _id: task._id },
      {
        $pop: { subTasks: -1 },
        progress: 0,
      }
    ),
    ProjectVersion.updateOne({ _id: projectVersionId }, versionDocument),
  ]);
  return undefined;
}
/**
 * 提交运行时检查任务
 * @param {Task} task 任务
 * @param {Array} files 文件列表
 * @param {Array} funcs 函数列表
 * @param {Object} project 项目信息
 * @param {Object} projectVersion 项目版本信息
 * @return {Promise}
 */
function submitRunTimeTask(task, files, funcs, project, projectVersion) {
  return new Promise((resolve, reject) => {
    hydra.prove(files, funcs, project, projectVersion, task)
      .then((hydraTask) => {
        if (!hydraTask) {
          return updateTaskStatus(task._id, taskStatusKind.finished);
        }
        const projectVersionId = projectVersion._id; // eslint-disable-line no-underscore-dangle
        const taskId = hydraTask.task_id;
        const versionDocument = { $set: {} };
        versionDocument.$set[`tasks.${taskId}`] = true;
        const promises = [
          updateTaskId(task, taskId),
          File.updateMany({ _id: { $in: task.fileIds } }, { analyzeTaskId: taskId }),
          ProjectVersion.updateOne({ _id: projectVersionId }, versionDocument),
        ];
        return Promise.all(promises);
      })
      .then(resolve)
      .catch(reject);
  });
}
/**
 * 添加运行时检查任务
 * @param {Array} files 文件列表
 * @param {Array} funcs 未被执行到的函数
 * @param {Object} project 项目信息
 * @param {Object} projectVersion 项目版本信息
 * @param {Object} creator 创建用户
 * @param {Array[number]} ruleIds 规则id
 * @return {Promise}
 */
function addRunTimeCheckTask(files, funcs, project, projectVersion, creator, ruleIds) {
  return new Promise((resolve, reject) => {
    let task;
    const fileIds = files.map(({ _id }) => _id);
    addTask(taskType.runtimeCheck, { fileIds, ruleIds }, project, projectVersion, creator)
      .then((res) => {
        task = res;
        return submitRunTimeTask(task, files, funcs, project, projectVersion);
      })
      .then(() => {
        resolve(task);
      })
      .catch(reject);
  });
}
/**
 * 提交静态分析任务
 * @param {Task} task 任务
 * @param {Array} files 文件列表
 * @param {Object} project 项目信息
 * @param {Object} projectVersion 项目版本信息
 * @return {Promise}
 */
function submitAnalyzeTask(task, files, project, projectVersion) {
  return new Promise((resolve, reject) => {
    hydra.analyze(files, project, projectVersion, task)
      .then((hydraTask) => {
        if (!hydraTask) {
          return updateTaskStatus(task._id, taskStatusKind.finished);
        }
        const taskId = hydraTask.task_id;
        return updateTaskId(task, taskId)
          .then((res) => {
            if (res.lastErrorObject.n === 0) {
              return Promise.resolve();
            }
            const promises = [
              Task.findOneAndUpdate(
                { _id: task._id },
                { $pop: { subTasks: -1 }, progress: 0 }
              ),
            ];
            const versionDocument = { $set: {} };
            versionDocument.$set[`tasks.${taskId}`] = true;
            promises.push(File.updateMany({ _id: { $in: task.fileIds } }, { analyzeTaskId: taskId }));
            promises.push(ProjectVersion.updateOne({ _id: projectVersion._id }, versionDocument));
            return Promise.all(promises);
          });
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 添加解析任务
 * @param {Array} files 文件列表
 * @param {Array} compilingFiles 可编译文件列表
 * @param {Object} project 项目信息
 * @param {Object} projectVersion 项目版本信息
 * @param {Object} creator 创建用户
 * @return {Promise}
 */
function addParseTask(files, compilingFiles, project, projectVersion, creator) {
  return new Promise((resolve, reject) => {
    let task;
    const fileIds = files.map(({ _id }) => _id);
    const compilingFileIds = compilingFiles.map(({ _id }) => _id);
    addTask(taskType.parse, { fileIds, compilingFileIds, currentSubTask: taskType.parse }, project, projectVersion, creator)
      .then((res) => {
        task = res;
        return submitParseTask(task, compilingFiles, project, projectVersion);
      })
      .then(() => {
        resolve(task);
      })
      .catch(reject);
  });
}

/**
 * 添加代码插桩任务
 * @param {String} codeInstrumentationId
 * @param {Array} fileFuncs
 * @param {Object} project
 * @param {Object} projectVersion
 * @param {Object} creator 创建用户
 */
async function addInstrumentTask(codeInstrumentationId, fileFuncs, project, projectVersion, creator) {
  const fileIds = [];
  const functionIds = [];
  const compilingFiles = [];
  const compilingFileIds = [];
  fileFuncs.forEach((item) => {
    fileIds.push(item.fileId);
    item.functions.forEach((f) => functionIds.push(f.functionId));
    if (item.status !== files.statusKind.compiled && item.status !== files.statusKind.compileFailed) {
      compilingFiles.push({ path: item.filePath });
      compilingFileIds.push(item.fileId);
    }
  });
  const doc = { fileIds, functionIds, codeInstrumentationId };
  if (compilingFiles.length > 0) {
    doc.subTasks = [taskType.instrument];
    doc.currentSubTask = taskType.parse;
    doc.compilingFileIds = compilingFileIds;
  }
  const task = await addTask(taskType.instrument, doc, project, projectVersion, creator);
  if (compilingFiles.length > 0) {
    await submitParseTask(task, compilingFiles, project, projectVersion);
  } else {
    await submitInstrumentTask(task, fileFuncs, project, projectVersion);
  }
  return task;
}

/**
 * 添加静态分析任务
 * @param {Array} files 文件列表
 * @param {Array} compilingFiles 可编译文件列表
 * @param {Object} project 项目信息
 * @param {Object} projectVersion 项目版本信息
 * @param {Object} creator 创建用户
 * @return {Promise}
 */
function addAnalyzeTask(files, compilingFiles, project, projectVersion, creator) {
  return new Promise((resolve, reject) => {
    let task;
    const doc = {
      fileIds: files.map(({ _id }) => _id),
    };
    // if (compilingFiles.length > 0) {
    //   doc.subTasks = [taskType.analyze];
    //   doc.currentSubTask = taskType.parse;
    //   doc.compilingFileIds = compilingFiles.map(({ _id }) => _id);
    // }
    addTask(taskType.analyze, doc, project, projectVersion, creator)
      .then((res) => {
        task = res;
        // if (compilingFiles.length > 0) {
        //   return submitParseTask(res, compilingFiles, project, projectVersion);
        // }
        return submitAnalyzeTask(task, files, project, projectVersion);
      })
      .then(() => {
        resolve(task);
      })
      .catch(reject);
  });
}

/**
 * 添加一键回归分析任务
 * @param {Array} files 文件列表
 * @param {Object} project 项目信息
 * @param {Object} projectVersion 项目版本信息
 * @param {Object} creator 创建用户
 * @return {Promise}
 */
function addRegressionAnalyzeTask(files, project, projectVersion, creator) {
  return new Promise((resolve, reject) => {
    let task;
    const fileIds = files.map(({ _id }) => _id);
    addTask(taskType.regressionAnalyze, { fileIds }, project, projectVersion, creator)
      .then((res) => {
        task = res;
        return submitAnalyzeTask(task, files, project, projectVersion);
      })
      .then(() => {
        resolve(task);
      })
      .catch(reject);
  });
}

/**
 * 提交生成任务
 * @param {Task} task 任务
 * @param {Array} functions 函数列表
 * @param {Map} fileMap 文件集合
 * @param {Object} project 项目
 * @param {Object} projectVersion 项目版本
 * @return {Promise}
 */
async function submitGenerateTask(task, functions, fileMap, project, projectVersion) {
  let taskId = null;
  const license = (await License.find().lean())[0];
  if (!license || !license.modules || !license.modules[projectVersion.versionType]) {
    throw new PhoenixError('not found', 'license not found !');
  }
  const ableGenerateTestFilter = license.modules[projectVersion.versionType].generateTest ? license.modules[projectVersion.versionType].generateTest : ['c', 'c++'];
  const fileIdsSet = new Set();
  task.functionIds = [];
  task.fileIds = [];
  const hydraTask = await hydra.generateTests(functions.filter((func) => {
    if (!ableGenerateTestFilter.includes(func.language)) {
      return false;
    }
    task.functionIds.push(func._id);
    if (!fileIdsSet.has(func.fileId.toString())) {
      task.fileIds.push(func.fileId);
      fileIdsSet.add(func.fileId.toString());
    }
    return true;
  }), fileMap, project, projectVersion, task);
  if (hydraTask) {
    taskId = hydraTask.task_id;
  }
  const res = await updateTaskId(task, taskId);
  if (res.lastErrorObject.n === 0) {
    return Promise.resolve();
  }
  const promise = [
    Task.findOneAndUpdate(
      { _id: task._id },
      {
        $pop: { subTasks: -1 },
        progress: 0,
        functionIds: task.functionIds,
        fileIds: task.fileIds,
      },
      { new: true }
    ),
  ];
  if (!taskId) {
    // when start auto test with c++
    // the above hydra api will return null if there is no c file.
    // so need to continue with next subtask
    // TODO optimize
    const [updatedTask] = await Promise.all(promise);
    // eslint-disable-next-line no-use-before-define
    return runNextTask(updatedTask);
  }
  const projectVersionId = projectVersion._id;
  const versionDocument = { $set: {} };
  versionDocument.$set[`tasks.${taskId}`] = true;
  promise.push(Func.updateMany({ _id: { $in: task.functionIds } }, { taskId }));
  promise.push(ProjectVersion.updateOne({ _id: projectVersionId }, versionDocument));
  return Promise.all(promise);
}

/**
 * 添加测试用例生成任务
 * @param {Array} functions 函数列表
 * @param {Map} fileMap 文件集合
 * @param {Object} project 项目
 * @param {Object} projectVersion 项目版本
 * @param {Object} creator 创建用户
 * @return {Promise}
 */
function addGenerateTask(functions, fileMap, project, projectVersion, creator) {
  return new Promise((resolve, reject) => {
    let task;
    const doc = {
      functionIds: functions.map(({ _id }) => _id),
      fileIds: [],
    };
    fileMap.forEach((file) => {
      doc.fileIds.push(file._id); // eslint-disable-line no-underscore-dangle
    });
    addTask(taskType.testCaseGenerate, doc, project, projectVersion, creator)
      .then((res) => {
        task = res;
        return submitGenerateTask(task, functions, fileMap, project, projectVersion);
      })
      .then(() => {
        resolve(task);
      })
      .catch(reject);
  });
}

/**
 * 提交执行任务
 * @param {Task} task 任务
 * @param {Array} testCases 测试用例列表
 * @param {Map} fileMap 文件集合
 * @param {Map} functionMap 函数集合
 * @param {Object} project 项目
 * @param {Object} projectVersion 项目版本
 * @param {Boolean} withoutCoverage 是否收集覆盖率
 * @param {Array} groupIds 测试用例group id列表
 * @param {ObjectId} lastRunFunctionId 此次执行到哪一个function id；为空则说明是分批执行到最后一批 或者没分批
 * @return {Promise}
 */
function submitRunTask(
  task,
  testCases,
  fileMap,
  functionMap,
  project,
  projectVersion,
  withoutCoverage,
  groupIds,
  lastRunFunctionId,
  firstRunFunctionId,
) {
  firstRunFunctionId = firstRunFunctionId || task.functionIds[0];
  const lastFunId = utility.lastItem(task.functionIds);
  lastRunFunctionId = lastRunFunctionId || lastFunId;
  const isFirstBatch = !task.lastRunFunctionId;
  const testCaseIds = [];
  const functionIdsSet = new Set();
  // eslint-disable-next-line no-restricted-syntax
  for (const testCase of testCases) {
    testCaseIds.push(testCase._id);
    functionIdsSet.add(testCase.functionId.toString());
  }
  return new Promise((resolve, reject) => {
    let taskId;
    const promise = task.testType === testType.integration
      ? hydra.runIntegrationTests(
        testCases,
        fileMap,
        functionMap,
        project,
        projectVersion,
        task,
        withoutCoverage,
        groupIds
      )
      : hydra.runTests(testCases, fileMap, functionMap, project, projectVersion, task, withoutCoverage, groupIds);
    promise
      .then((hydraTask) => {
        if (!hydraTask) {
          return updateTaskStatus(task._id, taskStatusKind.finished);
        }
        taskId = hydraTask.task_id;
        return updateTaskId(task, taskId, !isFirstBatch)
          .then((res) => {
            if (res.lastErrorObject.n === 0) {
              return Promise.resolve();
            }
            const projectVersionId = projectVersion._id;
            const versionDocument = { $set: {} };
            versionDocument.$set[`tasks.${taskId}`] = true;
            const promise = [
              TestCase.updateMany({ _id: { $in: testCaseIds } }, { taskId }),
              Func.updateMany({ _id: { $in: [...functionIdsSet].map(mongoose.Types.ObjectId) } }, { taskId }),
              ProjectVersion.updateOne({ _id: projectVersionId }, versionDocument),
              isFirstBatch
                ? Task.updateOne({ _id: task._id }, {
                  $pop: { subTasks: -1 }, progress: 0, lastRunFunctionId, firstRunFunctionId,
                })
                : Task.updateOne({ _id: task._id }, { progress: 0, lastRunFunctionId, firstRunFunctionId }),
            ];
            return Promise.all(promise);
          });
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 提交导出目标机测试脚本任务
 * @param {Task} task 任务
 * @param {Array} testCases 测试用例列表
 * @param {Map} fileMap 文件集合
 * @param {Map} functionMap 函数集合
 * @param {Object} project 项目
 * @param {Object} projectVersion 项目版本
 * @param {Boolean} withoutCoverage 是否收集覆盖率
 * @return {Promise}
 */
function submitExportTargetMachineTestTask(
  task, testCases, fileMap, functionMap, project, projectVersion, withoutCoverage
) {
  return new Promise((resolve, reject) => {
    let taskId;
    const promise = task.testType === testType.integration
      ? hydra.exportTargetMachineIntegrationTests(
        testCases, fileMap, functionMap, project, projectVersion, task, withoutCoverage
      )
      : hydra.exportTargetMachineTests(
        testCases, fileMap, functionMap, project, projectVersion, task, withoutCoverage
      );
    promise
      .then((hydraTask) => {
        if (!hydraTask) {
          return updateTaskStatus(task._id, taskStatusKind.finished);
        }
        taskId = hydraTask.task_id;
        return updateTaskId(task, taskId)
          .then((res) => {
            if (res.lastErrorObject.n === 0) {
              return Promise.resolve();
            }
            const projectVersionId = projectVersion._id;
            const versionDocument = { $set: {} };
            versionDocument.$set[`tasks.${taskId}`] = true;
            const promise = [
              TestCase.updateMany({ _id: { $in: task.testIds } }, { taskId }),
              Func.updateMany({ _id: { $in: task.functionIds } }, { taskId }),
              ProjectVersion.updateOne({ _id: projectVersionId }, versionDocument),
              Task.updateOne({ _id: task._id }, { $pop: { subTasks: -1 }, progress: 0 }),
            ];
            return Promise.all(promise);
          });
      })
      .then(resolve)
      .catch(reject);
  });
}

/**
 * 添加测试用例执行任务
 * @param {Array} testCases 测试用例列表
 * @param {Map} fileMap 文件集合
 * @param {Map} functionMap 函数集合
 * @param {Object} project 项目
 * @param {Object} projectVersion 项目版本
 * @param {Object} creator 创建用户
 * @param {Boolean} coverageStatistics 是否统计覆盖率
 * @param {Array} groupIds 待测顶层编组Id
 * @return {Promise}
 */
function addRunTask(
  testCases,
  fileMap,
  functionMap,
  project,
  projectVersion,
  creator,
  coverageStatistics = true,
  {
    groupIds = undefined,
    testCaseIds = undefined,
    functionId = undefined,
  } = {},
) {
  return new Promise((resolve, reject) => {
    const doc = {
      testIds: testCases.map(({ _id }) => _id),
      functionIds: [],
      fileIds: [],
      coverageStatistics,
      subTasks: [taskType.testCaseRun],
    };
    if (functionId) {
      doc.partiallyExecutedIds = {
        groupIds,
        testCaseIds,
        functionId,
      };
    }
    functionMap.forEach((func) => {
      doc.functionIds.push(func._id); // eslint-disable-line no-underscore-dangle
    });
    fileMap.forEach((file) => {
      doc.fileIds.push(file._id); // eslint-disable-line no-underscore-dangle
    });
    let task;
    addTask(taskType.testCaseRun, doc, project, projectVersion, creator)
      .then((res) => {
        task = res;
        if (functionId) {
          // 部分执行, 保持原逻辑, 不分批
          return submitRunTask(
            task,
            testCases,
            fileMap,
            functionMap,
            project,
            projectVersion,
            !Number(coverageStatistics),
            groupIds
          );
        }
        // eslint-disable-next-line no-use-before-define
        return runNextBatch(task);
      })
      .then(() => {
        resolve(task);
      })
      .catch(reject);
  });
}

/**
 * 添加导出目标机测试脚本任务
 * @param {Array} testCases 测试用例列表
 * @param {Map} fileMap 文件集合
 * @param {Map} functionMap 函数集合
 * @param {Object} project 项目
 * @param {Object} projectVersion 项目版本
 * @param {Object} creator 创建用户
 * @param {Boolean} coverageStatistics 是否统计覆盖率
 * @param {String} codeInstrumentationId 插桩id
 * @return {Promise}
 */
function addExportTargetMachineTestTask(
  testCases,
  fileMap,
  functionMap,
  project,
  projectVersion,
  creator,
  coverageStatistics,
  codeInstrumentationId,
) {
  const doc = {
    testIds: testCases.map(({ _id }) => _id),
    functionIds: [],
    fileIds: [],
    coverageStatistics,
    codeInstrumentationId,
  };
  functionMap.forEach((func) => {
    doc.functionIds.push(func._id); // eslint-disable-line no-underscore-dangle
  });
  fileMap.forEach((file) => {
    doc.fileIds.push(file._id); // eslint-disable-line no-underscore-dangle
  });
  let task;
  return addTask(taskType.exportTargetMachineTests, doc, project, projectVersion, creator)
    .then((res) => {
      task = res;
      return submitExportTargetMachineTestTask(
        task, testCases, fileMap, functionMap, project, projectVersion, !Number(coverageStatistics)
      );
    })
    .then(() => task);
}

/**
 * 启动一键测试的生成任务
 * @param {Task} task 任务
 * @return {Promise}
 */
function startAutoGenerateTask(task) {
  return new Promise((resolve, reject) => {
    const conditions = {
      $or: [
        { fileId: { $in: task.compilingFileIds } },
        { _id: { $in: task.functionIds } },
      ],
    };

    let functions;
    Func.find(conditions)
      .then((res) => {
        functions = res;
        const functionIds = [];
        functions.forEach((func) => {
          if (fileElements.isTestableElement(func.kind)) {
            functionIds.push(func._id);
          }
        });
        const doc = {
          functionIds,
          status: taskStatusKind.waiting,
          $inc: { spent: moment() - task.resumeAt },
          resumeAt: moment(),
          subTaskStatus: subTaskStatusKind.running,
        };
        return Task.updateOne({ _id: task._id }, doc); // eslint-disable-line no-underscore-dangle
      })
      .then(() => {
        const promises = [
          File.find({ _id: { $in: task.fileIds } }),
          Task.findOne({ _id: task._id }), // eslint-disable-line no-underscore-dangle
          Project.findOne({ _id: task.projectId }),
          ProjectVersion.findOne({ _id: task.versionId }),
        ];
        return Promise.all(promises);
      })
      .then(([resFiles, task, project, projectVersion]) => {
        const fileMap = new Map();
        resFiles.forEach((file) => {
          fileMap.set(file._id.toString(), file); // eslint-disable-line no-underscore-dangle
        });
        return submitGenerateTask(
          task,
          fileElements.filterTestUngenerableElements(functions),
          fileMap,
          project,
          projectVersion
        );
      })
      .then(resolve)
      .catch(reject);
  });
}

async function runNextBatch(task, batchSize) {
  batchSize = batchSize || (task.testType === testType.integration ? 50 : 100);
  let lastFunId;
  const funIdSet = new Set();
  const fileIdSet = new Set();
  const testCases = [];
  const fromIdx = task.lastRunFunctionId ? task.functionIds.findIndex((item) => task.lastRunFunctionId.equals(item)) + 1 : 0;
  for (let i = fromIdx; i < task.functionIds.length; i += 1) {
    const fid = task.functionIds[i];
    // eslint-disable-next-line no-await-in-loop
    const arr = await TestCase.find({ functionId: fid }).lean();
    if (arr.length) {
      testCases.push(...arr);
      funIdSet.add(fid.toString());
      fileIdSet.add(arr[0].fileId.toString());
      lastFunId = fid;
    }
    if (testCases.length >= batchSize) {
      break;
    }
  }
  const funcIds = [...funIdSet].map(mongoose.Types.ObjectId);
  const fileIds = [...fileIdSet].map(mongoose.Types.ObjectId);
  const [
    resFiles,
    resFunctions,
    functionVariables,
    task1,
    project,
    projectVersion,
    elementsSpecs,
    fileTypeSystems,
  ] = await Promise.all([
    File.find({ _id: { $in: fileIds } }),
    Func.find({ _id: { $in: funcIds } }),
    FunctionVariable.find({ functionId: { $in: funcIds } }),
    Task.findOne({ _id: task._id }).lean(), // eslint-disable-line no-underscore-dangle
    Project.findOne({ _id: task.projectId }),
    ProjectVersion.findOne({ _id: task.versionId }),
    fileElements.getSpecializedParams(funcIds),
    files.getTypes(fileIds),
  ]);
  const fileTypeSystemMap = new Map();
  fileTypeSystems.forEach((type) => {
    fileTypeSystemMap.set(type.fileId.toString(), type);
  });
  const fileMap = new Map();
  resFiles.forEach((file) => {
    file.fileTypeSystem = fileTypeSystemMap.has(file._id.toString())
      ? fileTypeSystemMap.get(file._id.toString()) : {};
    fileMap.set(file._id.toString(), file); // eslint-disable-line no-underscore-dangle
  });
  const functionMap = new Map();
  resFunctions.forEach((func) => {
    functionMap.set(func._id.toString(), func); // eslint-disable-line no-underscore-dangle
  });
  functionVariables.forEach((variables) => {
    functionMap.get(variables.functionId.toString()).variables = variables;
  });
  elementsSpecs.forEach((elementSpecs, functionId) => {
    functionMap.get(functionId).specs = elementSpecs;
  });
  // console.time('submitRunTask');
  logger.verbose('lastFunId', lastFunId);
  await submitRunTask(task1, testCases, fileMap, functionMap, project, projectVersion, !task.coverageStatistics, undefined,
    lastFunId, task.functionIds[fromIdx]);
  // console.timeEnd('submitRunTask');
}

/**
 * 启动一键测试的执行任务
 * @param {Task} task 任务
 * @return {Promise}
 */
async function startAutoRunTask(task) {
  const arr = await TestCase.find({ functionId: { $in: task.functionIds } }, '_id').lean();
  const doc = {
    testIds: arr.map(({ _id }) => _id),
    status: taskStatusKind.waiting,
    $inc: { spent: moment() - task.resumeAt },
    resumeAt: moment(),
    subTaskStatus: subTaskStatusKind.running,
  };
  await Task.updateOne({ _id: task._id }, doc);
}

async function submitMigrateTask(task, functions, versionId) {
  const taskId = mongoose.Types.ObjectId().toString();
  const { queue } = global.config.amqp;
  const resTask = await updateTaskId(task, taskId);
  if (resTask.lastErrorObject.n === 0) {
    return Promise.resolve();
  }
  const promise = [
    Task.findOneAndUpdate(
      { _id: task._id },
      { $pop: { subTasks: -1 }, progress: 0 },
      { new: true }
    ),
  ];
  const versionDoc = { $set: {} };
  versionDoc.$set[`tasks.${taskId}`] = true;
  promise.push(
    ProjectVersion.updateOne({ _id: mongoose.Types.ObjectId(versionId) }, versionDoc)
  );
  promise.push(Func.updateMany({ _id: { $in: task.functionIds } }, { taskId }));
  const [updatedTask] = await Promise.all(promise);
  if (utility.isEmpty(functions)) {
    // eslint-disable-next-line no-use-before-define
    return runNextTask(updatedTask);
  }
  await amqp.sendToQueue(
    queue,
    {
      state: taskStatusKind.running,
    },
    {
      headers: {
        'task-id': taskId,
        'tester-id': task._id.toString(),
        'message-type': 'progress',
        'ack-no': 1,
      },
    }
  );
  await utility.arrayChunkOperation(functions, 100, (func, index) => amqp.sendToQueue(
    queue,
    {
      functionId: func._id.toString(),
      fileId: func.fileId.toString(),
    },
    {
      headers: {
        'task-id': taskId,
        'tester-id': task._id.toString(),
        'message-type': 'migrate-testcase',
        'ack-no': index + 2,
      },
    }
  ));
  return amqp.sendToQueue(
    queue,
    {
      state: taskStatusKind.finished,
    },
    {
      headers: {
        'task-id': taskId,
        'tester-id': task._id.toString(),
        'message-type': 'progress',
        'ack-no': functions.length + 2,
      },
    }
  );
}

async function startAutoMigrateTask(task) {
  const conditions = {
    $or: [
      { fileId: { $in: task.compilingFileIds } },
      { _id: { $in: task.functionIds } },
    ],
  };

  const functions = await Func.find(conditions);
  const doc = {
    functionIds: functions.map(({ _id }) => _id),
    status: taskStatusKind.waiting,
    $inc: { spent: moment() - task.resumeAt },
    resumeAt: moment(),
    subTaskStatus: subTaskStatusKind.running,
  };
  task = await Task.findOneAndUpdate({ _id: task._id }, doc, { new: true });
  return submitMigrateTask(
    task,
    fileElements.filterTestUngenerableElements(functions),
    task.versionId,
  );
}

/**
 * 添加一键测试任务
 * @param {Array} files 文件列表
 * @param {Array} compilingFiles 可编译文件列表
 * @param {Array} functions 函数列表
 * @param {Object} project 项目
 * @param {Object} projectVersion 项目版本
 * @param {Object} creator 创建用户
 * @param {Boolean} coverageStatistics 是否统计覆盖率
 * @param {Boolean} deleteDuplicateCases 是否删除与手动添加的测试用例重复覆盖率的自动生成测试用例
 * @return {Promise}
 */
function addAutoTestTask(
  files,
  compilingFiles,
  functions,
  project,
  projectVersion,
  creator,
  coverageStatistics,
  deleteDuplicateCases
) {
  return new Promise((resolve, reject) => {
    const doc = {
      functionIds: functions.map(({ _id }) => _id),
      fileIds: files.map(({ _id }) => _id),
      compilingFileIds: compilingFiles.map(({ _id }) => _id),
      subTasks: [taskType.testCaseGenerate, taskType.testCaseRun],
      currentSubTask: taskType.parse,
      coverageStatistics,
    };
    const fileIdSet = new Set();
    files.forEach(({ _id }) => {
      fileIdSet.add(_id.toString());
    });
    functions.forEach(({ fileId }) => {
      if (!fileIdSet.has(fileId.toString())) {
        fileIdSet.add(fileId.toString());
        doc.fileIds.push(fileId);
      }
    });
    let task;
    const type = deleteDuplicateCases ? taskType.autoTestAndFilter : taskType.autoTest;
    addTask(type, doc, project, projectVersion, creator)
      .then((res) => {
        task = res;
        if (compilingFiles.length > 0) {
          return submitParseTask(task, compilingFiles, project, projectVersion);
        }
        return updateTaskId(task, 1)
          .then(() => startAutoGenerateTask(task));
      })
      .then(() => {
        resolve(task);
      })
      .catch(reject);
  });
}

function addAutoRegressionTest(
  files,
  compilingFiles,
  functions,
  project,
  projectVersion,
  creator,
) {
  const doc = {
    functionIds: functions.map(({ _id }) => _id),
    fileIds: files.map(({ _id }) => _id),
    compilingFileIds: compilingFiles.map(({ _id }) => _id),
    subTasks: [taskType.testCaseMigrate, taskType.testCaseRun],
  };
  const fileIdSet = new Set();
  files.forEach(({ _id }) => {
    fileIdSet.add(_id.toString());
  });
  functions.forEach(({ fileId }) => {
    if (!fileIdSet.has(fileId.toString())) {
      fileIdSet.add(fileId.toString());
      doc.fileIds.push(fileId);
    }
  });
  let task;
  return addTask(taskType.autoRegression, doc, project, projectVersion, creator)
    .then((res) => {
      task = res;
      if (compilingFiles.length > 0) {
        return submitParseTask(task, compilingFiles, project, projectVersion);
      }
      return updateTaskId(task, 1)
        .then(() => startAutoMigrateTask(task));
    })
    .then(() => task);
}

/**
 * 继续任务
 * @param {Object} task 任务
 * @return {Promise}
 */
async function runNextTask(task) {
  if (
    [
      taskStatusKind.canceling,
      taskStatusKind.canceled,
      taskStatusKind.suspending,
      taskStatusKind.suspended,
      taskStatusKind.finished,
    ].indexOf(task.status) >= 0
  ) {
    return Promise.resolve();
  }
  if (task.lastRunFunctionId && !task.lastRunFunctionId.equals(utility.lastItem(task.functionIds))) {
    // 分批执行中
    return runNextBatch(task);
  }
  if (task.subTasks.length === 0) {
    return updateTaskStatus(task._id, taskStatusKind.finished);
  }
  const nextTask = task.subTasks.shift();
  // 更新currentSubTask
  await Task.updateOne({ _id: task._id }, {
    $set: { currentSubTask: nextTask },
  });
  let files;
  let funcs;
  let project;
  let projectVersion;
  switch (nextTask) {
    case taskType.testCaseGenerate:
      return startAutoGenerateTask(task);
    case taskType.testCaseRun:
      // eslint-disable-next-line no-case-declarations
      let promise = Promise.resolve();
      if (!task.lastRunFunctionId) {
        // 第一次分批执行
        promise = promise.then(() => startAutoRunTask(task));
      }
      return promise.then(() => runNextBatch(task));
    case taskType.testCaseMigrate:
      return startAutoMigrateTask(task);
    case taskType.analyze:
      // eslint-disable-next-line no-case-declarations
      [files, project, projectVersion] = await Promise.all(
        [
          File.find({ _id: { $in: task.fileIds } }, ['path']).lean(),
          Project.findById(task.projectId),
          ProjectVersion.findById(task.versionId),
        ]
      );
      return submitAnalyzeTask(task, files, project, projectVersion);
    case taskType.instrument:
      [files, funcs, project, projectVersion] = await Promise.all(
        [
          File.find({ _id: { $in: task.fileIds } }, ['path']).lean(),
          Func.find(task.functionIds.length === 0 ? { fileId: { $in: task.fileIds } } : { _id: { $in: task.functionIds } }, ['functionName', 'mangledId', 'language', 'fileId', 'kind']).lean(),
          Project.findById(task.projectId),
          ProjectVersion.findById(task.versionId),
        ]
      );
      // eslint-disable-next-line no-case-declarations
      const fileFuncsMap = new Map();
      funcs.forEach((func) => {
        if (!fileFuncsMap.has(func.fileId.toString())) {
          fileFuncsMap.set(func.fileId.toString(), []);
        }
        fileFuncsMap.get(func.fileId.toString()).push({
          functionName: func.functionName,
          mangledId: func.mangledId,
          language: func.language,
          kind: func.kind,
        });
      });
      return submitInstrumentTask(task, files.map((file) => ({
        filePath: file.path,
        functions: fileFuncsMap.get(file._id.toString()) || [],
      })), project, projectVersion);
    case taskType.parse:
    default:
      throw new Error(`invalid task type ${nextTask}`);
  }
}

async function submitCodeInstrumentationReportGenTask(
  task,
  codeInstrumentation,
  resultIds,
  project,
  projectVersion
) {
  const statisticalFuncs = {};
  await Promise.all(codeInstrumentation.files.map(async (file) => {
    if (!statisticalFuncs[file.filePath]) {
      statisticalFuncs[file.filePath] = [];
    }
    let funcs;
    if (!file.functions || file.functions.length === 0) {
      funcs = await functions.getFunctions(projectVersion._id, {}, file.fileId);
    } else {
      funcs = file.functions;
    }
    funcs.forEach((func) => {
      statisticalFuncs[file.filePath].push(functions.isCFunction(func) ? func.functionName : func.mangledId);
    });
    return Promise.resolve();
  }));
  const hydraTask = await hydra.analyzeCodeInstrumentationResult(
    codeInstrumentation._id.toString(),
    statisticalFuncs,
    resultIds.map((id) => id.toString()),
    project,
    projectVersion,
    task
  );
  if (!hydraTask) {
    return updateTaskStatus(task._id, taskStatusKind.finished);
  }
  await updateTaskId(task, hydraTask.task_id);
  return Promise.resolve();
}

async function addCodeInstrumentationReportGenTask(
  codeInstrumentation,
  resultIds,
  project,
  projectVersion,
  creator,
) {
  await gitClient.createBranch(
    { _id: project.creatorId },
    project,
    projectVersion,
    codeInstrumentation._id.toString(),
    projectVersion.masterVersionId ? projectVersion.masterVersionId.toString() : projectVersion._id.toString()
  );
  await fs.copy(
    path.join(cacheUtil.getCodeInstrumentationDir(), projectVersion._id.toString(), `${codeInstrumentation._id}_results`),
    gitClient.getLocalRepoPath({ _id: project.creatorId }, project)
  );
  await gitClient.addAllFilesInDirectory('.', { _id: project.creatorId }, project);
  const doc = {
    codeInstrumentationId: codeInstrumentation._id,
  };
  const task = await addTask(taskType.codeInstrumentationReportGen, doc, project, projectVersion, creator);
  await submitCodeInstrumentationReportGenTask(task, codeInstrumentation, resultIds, project, projectVersion);
  return task;
}

/**
 * 自动补充这是用例任务
 * @param {String} taskId 主任务id/子任务id
 * @returns boolean
 */
async function isAutoTestAndFilterTask(taskId) {
  let task = await Task.findById(taskId);
  if (!task) {
    task = await Task.findOne({ taskId: { $in: [taskId] } });
  }
  return task.type === taskType.autoTestAndFilter;
}
async function getPendingTasks() {
  const conditions = {
    $and: [
      { status: { $ne: taskStatusKind.finished } },
      { status: { $ne: taskStatusKind.canceled } },
    ],
  };
  const arr = await Task.find(conditions);
  return arr;
}

eventHub.on(eventKind.EVENT_VERSION_REMOVE, (versionId) => {
  versionId = mongoose.Types.ObjectId(versionId);
  return Promise.all([
    Task.deleteMany({ versionId }),
    // TaskLog.deleteMany({ versionId }),
  ]);
});
/**
 * Export model definition object.
 */
module.exports = {
  type: taskType,
  statusKind: taskStatusKind,
  subStatusKind: subTaskStatusKind,

  getJson,

  getTask,
  getTaskByHydraTaskId,
  getProgress,
  getSpentTime,
  getTaskOptions,
  filter,
  getCount,

  updateTask,
  updateTasks,
  updateTaskStatus,
  updateProgress,
  updateProjectName,
  updateProjectVersionName,
  updateSubTaskStatus,
  addInstrumentTask,

  addTaskLog,
  addParseTask,
  addAnalyzeTask,
  addCodeInstrumentationReportGenTask,
  addRunTimeCheckTask,
  addRegressionAnalyzeTask,
  addGenerateTask,
  addRunTask,
  addExportTargetMachineTestTask,
  addAutoTestTask,
  addAutoRegressionTest,
  runNextTask,
  isSubTaskFinished,
  isAutoTestAndFilterTask,
  getPendingTasks,
};
exports = Object.assign(exports, module.exports);
